﻿using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Forms;

namespace FGrubert.CrmQueueMonitor.WinForms
{
    public partial class FrmMain : Form
    {
        /// <summary>
        /// Stores the organization service to use
        /// </summary>
        private IOrganizationService _orgService;
        /// <summary>
        /// The list of queues
        /// </summary>
        private SortableBindingList<Queue> _queues = new SortableBindingList<Queue>();
        /// <summary>
        /// Stores the version of the CRM Server to connect to
        /// </summary>
        private CrmVersion _crmVersion;
        /// <summary>
        /// Stores the list of the info configurations
        /// </summary>
        private Dictionary<Guid, InfoMonitor.WindowConfiguration> _infoConfigs = new Dictionary<Guid, InfoMonitor.WindowConfiguration>();

        private delegate void DlgSetQueueList();
        private DlgSetQueueList _setQueueList;

        public FrmMain()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Exit the application
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void menuItemExit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        /// <summary>
        /// Show the queue monitor main window
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void menuItemOpen_Click(object sender, EventArgs e)
        {
            this.Show();
            this.BringToFront();
        }

        /// <summary>
        /// Show the settings dialog
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void menuItemSettings_Click(object sender, EventArgs e)
        {
            FrmSettings frmSettings = new FrmSettings();
            if (frmSettings.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
            {
                this.Opacity = (double)Properties.Settings.PermanentDefault.Opacity / (double)100;
                InitializeChecks();

                FrmMain_Resize(this, null);
            }
        }

        /// <summary>
        /// Show the about dialog
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void menuItemAbout_Click(object sender, EventArgs e)
        {
            AboutBox aboutBox = new AboutBox();
            aboutBox.ShowDialog(this);
        }

        /// <summary>
        /// If user wants do close the window: just hide it. Safe the last position
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void FrmMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (this.Visible)
            {
                Properties.Settings settings = Properties.Settings.PermanentDefault;
                settings.LastPosition = this.Location;
                settings.LastSize = this.Size;
                settings.SavePermanent();
            }
            if (e.CloseReason == CloseReason.UserClosing)
            {
                this.Hide();
                e.Cancel = true;
            }
        }

        /// <summary>
        /// Show the queue monitor main window
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void notifier_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            this.Show();
            this.BringToFront();
        }

        /// <summary>
        /// Perform a new check
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timerCheck_Tick(object sender, EventArgs e)
        {
            if (!bgWorker.IsBusy)
            {
                bgWorker.RunWorkerAsync(true);
            }
        }

        /// <summary>
        /// Set Position and connect
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void FrmMain_Load(object sender, EventArgs e)
        {
            Properties.Settings settings = Properties.Settings.PermanentDefault;
            if (settings.LastPosition != new Point(0, 0))
            {
                this.Location = settings.LastPosition;
            }
            else
            {
                this.Left = Screen.PrimaryScreen.Bounds.Right - this.Width - 50;
                this.Top = Screen.PrimaryScreen.Bounds.Bottom - this.Height - 50;
            }
            if (settings.LastSize != new Size(0, 0))
            {
                this.Size = settings.LastSize;
            }
            if (string.IsNullOrEmpty(settings.CrmUrl) || settings.QueueConfigDoc == null)
            {
                FrmSettings frmSettings = new FrmSettings();
                frmSettings.ShowDialog(this);
            }
            timerCheck.Interval = settings.RefreshInterval;
            this.Opacity = (double)settings.Opacity / (double)100;

            dgvQueues.AutoGenerateColumns = false;
            _setQueueList = new DlgSetQueueList(SetQueueList);

            LoadInfoConfigs();

            System.Threading.Thread thread = new System.Threading.Thread(InitializeChecks);
            thread.Start();
        }

        /// <summary>
        /// Perform the queue layout
        /// </summary>
        private void SetQueueList()
        {
            if (dgvQueues.InvokeRequired)
            {
                dgvQueues.Invoke(_setQueueList);
            }
            else
            {
                dgvQueues.DataSource = _queues;
                dgvQueues.PerformLayout();

                progressBar.Enabled = false;
                progressBar.Visible = false;

                timerCheck.Enabled = true;
            }
        }

        /// <summary>
        /// Initializes the CRM Connection
        /// </summary>
        private void InitializeChecks()
        {
            try
            {
                timerCheck.Enabled = false;

                Properties.Settings settings = Properties.Settings.PermanentDefault;
                _queues.Clear();

                if (settings.QueueConfigs == null || settings.QueueConfigs.Count() <= 0)
                {
                    return;
                }

                _orgService = CrmConnector.GetCrmConnection(settings);
                _crmVersion = CrmConnector.GetCrmVersion(_orgService);

                QueueBase[] queueConfigs = settings.QueueConfigs;
                string[] selectedQueues = new string[queueConfigs.Count()];
                for (int i = 0; i < queueConfigs.Count(); i++)
                {
                    selectedQueues[i] = queueConfigs[i].QueueId.ToString();
                }

                // Get all queues first
                QueryExpression query = new QueryExpression("queue");
                query.ColumnSet = new ColumnSet("name", "queueid");

                FilterExpression filter = new FilterExpression();
                filter.AddCondition("queueid", ConditionOperator.In, selectedQueues);
                query.Criteria = filter;

                query.AddOrder("name", OrderType.Ascending);

                EntityCollection entities = _orgService.RetrieveMultiple(query);

                foreach (Entity entity in entities.Entities)
                {
                    Queue queue = new Queue();

                    QueueBase queueConfig = queueConfigs.FirstOrDefault(q => q.QueueId == entity.Id);
                    if (queueConfig != null)
                    {
                        queue = new Queue(queueConfig);
                    }

                    queue.QueueId = entity.Id;
                    queue.Name = entity.GetAttributeValue<string>("name");

                    _queues.Add(queue);
                }

                SetQueueList();

                if (!bgWorker.IsBusy)
                {
                    bgWorker.RunWorkerAsync(false);
                }

            }
            catch (Exception error)
            {
                notifier.ShowBalloonTip(3000, "An error occured", error.Message, ToolTipIcon.Error);
            }
        }

        /// <summary>
        /// Refreshes the values of the queues
        /// </summary>
        private void RefreshQueueValues(bool doPopup)
        {
            try
            {
                DateTime dataLoadTime = DateTime.Now;
                string updatedQueues = "";

                if (_queues == null || _queues.Count() <= 0)
                {
                    // No queues available
                    return;
                }

                // FetchXml to retreive all required data
                string fetchXml = GetQueueValueFetchXml();
                FetchExpression fetch = new FetchExpression(fetchXml);

                EntityCollection entities = _orgService.RetrieveMultiple(fetch);

                foreach (Entity entity in entities.Entities)
                {
                    Guid queueid = ((EntityReference)((AliasedValue)entity["queueid"]).Value).Id;
                    Queue queue = _queues.FirstOrDefault(q => q.QueueId == queueid);

                    if (queue != null)
                    {
                        queue.NumItems = (Int32)((AliasedValue)entity["queueitem_count"]).Value;
                        queue.NumNotWorkedOn = queue.NumItems - (Int32)((AliasedValue)entity["workerid_count"]).Value;

                        DateTime? newLatest = null;
                        DateTime? newOldest = null;

                        if (_crmVersion == CrmVersion.Crm2013 || _crmVersion == CrmVersion.After2013)
                        {
                            // Process CRM 2013 directly
                            newOldest = ((DateTime)((AliasedValue)entity["enteredon_min"]).Value).ToLocalTime();
                            newLatest = ((DateTime)((AliasedValue)entity["enteredon_max"]).Value).ToLocalTime();
                        }
                        else
                        {
                            // Process CRM 2011 separately due to missing aggregates
                            newOldest = GetDatesForQueueCrm2011(queue, out newLatest);
                        }

                        if (Properties.Settings.PermanentDefault.StatusCalculationMode == StatusCalculationMode.NotWorkedOn)
                        {
                            newOldest = GetNoWorkerDateForQueue(queue);
                            // Get the date of the oldest item that is not worked on
                        }

                        queue.Oldest = newOldest;

                        if (!queue.Latest.HasValue || newLatest > queue.Latest)
                        {
                            updatedQueues += queue.Name + Environment.NewLine;
                        }
                        queue.Latest = newLatest;

                        queue.LastUpdated = dataLoadTime;
                    }
                }

                foreach (Queue queue in _queues.Where(q => q.LastUpdated < dataLoadTime))
                {
                    // Reset all queues that don't contain any items
                    queue.NumItems = 0;
                    queue.NumNotWorkedOn = 0;
                    queue.Latest = null;
                    queue.Oldest = null;

                    queue.LastUpdated = dataLoadTime;
                }

                if (doPopup && !string.IsNullOrEmpty(updatedQueues))
                {
                    notifier.ShowBalloonTip(10000, "New items in queues", updatedQueues, ToolTipIcon.Info);
                }
            }
            catch (Exception error)
            {
                notifier.ShowBalloonTip(3000, "An error occured", error.Message, ToolTipIcon.Error);
            }

        }

        /// <summary>
        /// Get the min and max date for the given queue
        /// </summary>
        /// <param name="queue">The queue to process</param>
        /// <param name="newLatest">Returns the date of the latest queue item</param>
        /// <returns>The date of the oldest element in the queue</returns>
        private DateTime? GetDatesForQueueCrm2011(Queue queue, out DateTime? newLatest)
        {
            QueryExpression query = new QueryExpression("queueitem");
            query.ColumnSet = new ColumnSet("enteredon");

            FilterExpression filter = new FilterExpression();
            filter.FilterOperator = LogicalOperator.And;
            query.Criteria = filter;

            ConditionExpression condQueue = new ConditionExpression("queueid", ConditionOperator.Equal, queue.QueueId);
            filter.Conditions.Add(condQueue);
            ConditionExpression condState = new ConditionExpression("statecode", ConditionOperator.Equal, 0);
            filter.Conditions.Add(condState);
            ConditionExpression condEnteredOn = new ConditionExpression("enteredon", ConditionOperator.NotNull);
            filter.Conditions.Add(condEnteredOn);

            query.AddOrder("enteredon", OrderType.Ascending);

            EntityCollection entities = _orgService.RetrieveMultiple(query);

            Entity latest = entities.Entities.LastOrDefault();
            if (latest != null)
            {
                newLatest = latest.GetAttributeValue<DateTime>("enteredon").ToLocalTime();
            }
            else
            {
                newLatest = null;
            }

            DateTime? newOldest = null;
            Entity oldest = entities.Entities.FirstOrDefault();
            if (oldest != null)
            {
                newOldest = oldest.GetAttributeValue<DateTime>("enteredon").ToLocalTime();
            }

            return newOldest;
        }

        /// <summary>
        /// Get the min date for the given queue that has no worker
        /// </summary>
        /// <param name="queue">The queue to process</param>
        /// <returns>The date of the oldest item that has no worker</returns>
        private DateTime? GetNoWorkerDateForQueue(Queue queue)
        {
            QueryExpression query = new QueryExpression("queueitem");
            query.ColumnSet = new ColumnSet("enteredon");

            FilterExpression filter = new FilterExpression();
            filter.FilterOperator = LogicalOperator.And;
            query.Criteria = filter;

            ConditionExpression condQueue = new ConditionExpression("queueid", ConditionOperator.Equal, queue.QueueId);
            filter.Conditions.Add(condQueue);
            ConditionExpression condState = new ConditionExpression("statecode", ConditionOperator.Equal, 0);
            filter.Conditions.Add(condState);
            ConditionExpression condEnteredOn = new ConditionExpression("enteredon", ConditionOperator.NotNull);
            filter.Conditions.Add(condEnteredOn);
            ConditionExpression condNoWorker = new ConditionExpression("workerid", ConditionOperator.Null);
            filter.Conditions.Add(condNoWorker);

            query.AddOrder("enteredon", OrderType.Ascending);

            query.PageInfo.Count = 1;
            query.PageInfo.PageNumber = 1;

            EntityCollection entities = _orgService.RetrieveMultiple(query);

            DateTime? newOldest = null;
            Entity oldest = entities.Entities.FirstOrDefault();
            if (oldest != null)
            {
                newOldest = oldest.GetAttributeValue<DateTime>("enteredon").ToLocalTime();
            }

            return newOldest;

        }

        /// <summary>
        /// Create the fetchXml to retreive all required data
        /// </summary>
        /// <returns>A strign with the fetch xml</returns>
        private string GetQueueValueFetchXml()
        {
            string fetchXml = @" 
                <fetch distinct='false' mapping='logical' aggregate='true'> 
                    <entity name='queueitem'> 
                        <attribute name='queueitemid' alias='queueitem_count' aggregate='count' />
                        <attribute name='workerid' alias='workerid_count' aggregate='countcolumn' /> ";
            // Unfortunately CRM 2011 does not allow aggregation on Date Values where CRM 2013 does. So for 2013 we use a nice single fetch,
            // and for 2011 we have to do some extra fetching in the loop. I hope that code that works for 2013 will work after 2013 also
            if (_crmVersion == CrmVersion.Crm2013 || _crmVersion == CrmVersion.After2013)
            {
                fetchXml += @"
                        <attribute name='enteredon' alias='enteredon_max' aggregate='max' />
                        <attribute name='enteredon' alias='enteredon_min' aggregate='min' /> ";
            }
            fetchXml += @"
                        <attribute name='queueid' alias='queueid' groupby='true' /> 
                        <filter type='and'>
                            <condition attribute='statecode' operator='eq' value='0' />
                            <filter type='or'>";
            foreach (Queue queue in _queues)
            {
                fetchXml += @"
                                <condition attribute='queueid' operator='eq' value='" + queue.QueueId.ToString() + @"' />";
            }

            fetchXml += @"
                            </filter>
                        </filter>
                    </entity>
                </fetch>";
            return fetchXml;
        }

        /// <summary>
        /// An error occured during the binding
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void dgvQueues_DataError(object sender, DataGridViewDataErrorEventArgs e)
        {
            notifier.ShowBalloonTip(3000, "An error occured", e.Exception.Message, ToolTipIcon.Error);
            e.ThrowException = false;
        }

        /// <summary>
        /// Show the context menu
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void notifier_MouseClick(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                // We have to do it that way because via normal "show" the context menu doesn't close after clicking away
                MethodInfo mi = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
                mi.Invoke(notifier, null);
            }
        }

        /// <summary>
        /// The worker completed. Check if an error occured. If so, show the message
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                notifier.ShowBalloonTip(3000, "An error occured", e.Error.Message, ToolTipIcon.Error);
            }
        }

        /// <summary>
        /// Refresh the queue values in the background
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            RefreshQueueValues((bool)e.Argument);
        }

        /// <summary>
        /// If the form is resized and 
        /// </summary>
        private void FrmMain_Resize(object sender, EventArgs e)
        {
            WidthConfiguration newWidthConf = new WidthConfiguration(this.Width, Properties.Settings.PermanentDefault.EnableAutoscale);

            colStatus.Width = newWidthConf.ColStatusWidth;
            colName.Width = newWidthConf.ColNameWidth;
            colItems.Width = newWidthConf.ColItemsWidth;
            colNotWorkedOn.Width = newWidthConf.ColNotWorkedOnWidth;
            colLatest.Width = newWidthConf.ColLatestWidth;
            colLongest.Width = newWidthConf.ColLongestWidth;

            dgvQueues.DefaultCellStyle.Font = new Font(dgvQueues.DefaultCellStyle.Font.FontFamily, newWidthConf.FontSize, dgvQueues.DefaultCellStyle.Font.Style);
            dgvQueues.ColumnHeadersDefaultCellStyle.Font = new Font(dgvQueues.ColumnHeadersDefaultCellStyle.Font.FontFamily, newWidthConf.FontSize, dgvQueues.ColumnHeadersDefaultCellStyle.Font.Style);

            dgvQueues.ColumnHeadersHeight = newWidthConf.RowHeight + 1;
            dgvQueues.RowTemplate.Height = newWidthConf.RowHeight;
            foreach (DataGridViewRow row in dgvQueues.Rows)
            {
                row.Height = newWidthConf.RowHeight;
            }
        }

        #region Information Window handling

        /// <summary>
        /// Shows the dialog to add a new information window
        /// </summary>
        private void mnuItemAddNewInfoWindow_Click(object sender, EventArgs e)
        {
            InfoMonitor.WindowConfiguration config = new InfoMonitor.WindowConfiguration();

            InfoMonitor.FrmSettings frmInfo = new InfoMonitor.FrmSettings(config);
            if (frmInfo.ShowDialog(this) == DialogResult.OK)
            {
                AddInfoConfig(config);

                SaveInfoConfig();
            }
        }

        /// <summary>
        /// Saves the current information window configuration
        /// </summary>
        private void SaveInfoConfig()
        {
            Properties.Settings settings = Properties.Settings.PermanentDefault;
            settings.InfoWindowConfigs = _infoConfigs.Values.ToArray();
            settings.SavePermanent();
        }

        /// <summary>
        /// Adds a new info config to the list and creates the necessary windows and menu entries
        /// </summary>
        /// <param name="config">The configuration of the form</param>
        private void AddInfoConfig(InfoMonitor.WindowConfiguration config)
        {
            _infoConfigs.Add(config.Id, config);

            CreateWindow(config);
            
            ToolStripMenuItem windowMainMenu = new ToolStripMenuItem(config.WindowTitle);
            windowMainMenu.Tag = config;
            mnuItemInfoMonitor.DropDownItems.Add(windowMainMenu);

            ToolStripMenuItem settingsMenu = new ToolStripMenuItem("Settings");
            settingsMenu.Tag = config;
            settingsMenu.Click += infoWindowSettingsMenu_Click;
            windowMainMenu.DropDownItems.Add(settingsMenu);

            ToolStripMenuItem showHideMenu = new ToolStripMenuItem("Show/Hide");
            showHideMenu.Tag = config;
            showHideMenu.Click += infoWindowShowHideMenu_Click;
            windowMainMenu.DropDownItems.Add(showHideMenu);

            ToolStripMenuItem deleteMenu = new ToolStripMenuItem("Delete");
            deleteMenu.Tag = config;
            deleteMenu.Click += infoWindowDeleteMenu_Click;
            windowMainMenu.DropDownItems.Add(deleteMenu);
        }

        /// <summary>
        /// Creates a new config window for the given configuration
        /// </summary>
        /// <param name="config">The configuration object</param>
        private void CreateWindow(InfoMonitor.WindowConfiguration config)
        {
            config.Form = new InfoMonitor.FrmInfo(config);
            config.Form.FormClosing += InfoForm_FormClosing;
            if (config.Visible)
            {
                config.Form.Show();
            }
        }

        /// <summary>
        /// Delete the associated information window
        /// </summary>
        /// <param name="sender">The menu item that was clicked</param>
        /// <param name="e"></param>
        private void infoWindowDeleteMenu_Click(object sender, EventArgs e)
        {
            InfoMonitor.WindowConfiguration config = (InfoMonitor.WindowConfiguration)((ToolStripMenuItem)sender).Tag;

            if (MessageBox.Show("Do you really want to delete the form " + config.WindowTitle, "Delete Configuration", MessageBoxButtons.YesNo) == DialogResult.Yes)
            {
                config.Form.Close();
                config.Form.Dispose();
                config.Form = null;
                _infoConfigs.Remove(config.Id);

                foreach (ToolStripItem item in mnuItemInfoMonitor.DropDownItems)
                {
                    if (item.Tag == config)
                    {
                        mnuItemInfoMonitor.DropDownItems.Remove(item);
                        break;
                    }
                }

                SaveInfoConfig();
            }
        }

        /// <summary>
        /// Show or hide an information window
        /// </summary>
        /// <param name="sender">The menu item that was clicked</param>
        /// <param name="e"></param>
        private void infoWindowShowHideMenu_Click(object sender, EventArgs e)
        {
            InfoMonitor.WindowConfiguration config = (InfoMonitor.WindowConfiguration)((ToolStripMenuItem)sender).Tag;

            if (config.Form.Visible)
            {
                config.Form.Hide();
            }
            else
            {
                config.Form.Show();
            }
        }

        /// <summary>
        /// Show the settings dialog for an information window
        /// </summary>
        /// <param name="sender">The menu item that was clicked</param>
        /// <param name="e"></param>
        private void infoWindowSettingsMenu_Click(object sender, EventArgs e)
        {
            InfoMonitor.WindowConfiguration config = (InfoMonitor.WindowConfiguration)((ToolStripMenuItem)sender).Tag;

            InfoMonitor.FrmSettings frmInfo = new InfoMonitor.FrmSettings(config);
            if (frmInfo.ShowDialog(this) == DialogResult.OK)
            {
                // Close the old form
                config.Form.Close();
                config.Form.Dispose();
                config.Visible = true;

                CreateWindow(config);

                SaveInfoConfig();
            }
        }

        /// <summary>
        /// Before the information form is closed: Save the location and position and the visible items
        /// </summary>
        /// <param name="sender">The information form that caused the event</param>
        /// <param name="e">Information about the close reason</param>
        private void InfoForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            InfoMonitor.FrmInfo infoForm = (InfoMonitor.FrmInfo)sender;
            if (infoForm.Visible)
            {
                infoForm.Config.LastPosition = infoForm.Location;
                infoForm.Config.LastSize = infoForm.Size;
                if (e.CloseReason == CloseReason.UserClosing)
                {
                    e.Cancel = true;
                    infoForm.Config.Visible = false;
                    infoForm.Hide();
                }
                else
                {
                    infoForm.Config.Visible = true;
                }

                SaveInfoConfig();
            }
        }

        /// <summary>
        /// Load the infomation window configurations from the settings
        /// </summary>
        private void LoadInfoConfigs()
        {
            this._infoConfigs = new Dictionary<Guid, InfoMonitor.WindowConfiguration>();
            InfoMonitor.WindowConfiguration[] configs = Properties.Settings.PermanentDefault.InfoWindowConfigs;

            foreach (InfoMonitor.WindowConfiguration config in configs)
            {
                AddInfoConfig(config);
            }
        }

        #endregion
    }
}
